package gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.GrayFilter;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.border.LineBorder;
import javax.xml.crypto.dsig.Transform;

import advance.GameMap;

import map.AbstractMap;
import map.MapEvent;
import map.MapHeightEvent;
import map.MapListener;
import map.MapResolutionEvent;
import map.MapWidthEvent;
import mapelements.background.Background;
import mapelements.foreground.Foreground;
import mapelements.foreground.MapLayer;
import mapelements.foreground.MapObject;

import controller.StrataNumberUser;
import controller.StratumLevelUser;
import controller.gui.InsertionDisplayController;
import errors.DimensionNotAcceptableException;
import errors.StratumNotFoundException;
import sun.awt.image.*;

//FIXME bisogna creare un listener GameMapListener che quando la mappa cambia (aggiunta di foreground eccetera) il Display si automodifica di conseguenza!!!

/** Un Display è un componente dell'Editor pensato per visualizzare
 *  il progetto a cui sta lavorando l'utente. Al suo interno trovano
 *  posto due assi di allineamento su uno sfondo neutro.
 * @author Daniele
 * @version 2.3 */
public class Display extends JComponent implements Scrollable, MapListener, MouseMotionListener,StrataNumberUser,StratumLevelUser {

	private static final long serialVersionUID = -1461500059364328660L;
	/**rappresenta lo sfondo di default che compare in caso non ci sia un background disponibile per la mappa*/
	private BufferedImage defaultBackground;
	/**rappresenta l'incremento/decremente standard in caso in cui il Display non sia stato cablato con una mappa (map==null)*/
	public static final int DEFAULTSCROLLINCREMENT=50;

	/** La mappa che il Display deve visualizzare. */
	private GameMap map;
	/**rappresenta il punto indicato dal mouse*/
	private Point mouse;
	/**rappresenta una lista di punti di cui vanno disegnati i relativi segmenti*/
	private Vector<Point> points;
	/**indica l'immagine su cui viene disegnata la mappa prima di essere incollata sul Display*/
	private BufferedImage mapSheet;
	/**se TRUE ogni volta che il Display individua un MouseEvent effettua un repaint()*/
	private boolean repaintOnMouseMoved;
	
	/** Costruisce un Display completo di sfondo neutro. 
	 * @param _map  la mappa che il Display deve mostrare. */
	public Display(GameMap _map){
		super();
		try {
			this.defaultBackground=ImageIO.read(new File("mapeditor/tool/StandardNewBackground.png"));
		} catch (IOException error) {
			//immagine non trovata...
			this.defaultBackground=new BufferedImage(100,100,BufferedImage.TYPE_4BYTE_ABGR);
			this.defaultBackground.getGraphics().fillRect(0, 0, 100,100);
		}
		map = _map;
		this.repaintOnMouseMoved=false;
		this.points=new Vector<Point>(0,1);
		this.mouse=new Point(0,0);
		this.setPreferredSize(new Dimension(800,600));
		this.addMouseMotionListener(this);
	}
	
	/**costruisce un nuovo Display senza impostare la {@link #map}:
	 * Questa anddrà impostata tramite il comando {@link #setMap(AbstractMap)}.
	 * <p><strong>quando la {@link #map} è NULL la mappa non disegna nulla!</strong>*/
	public Display() {
		this(null);
		this.mapSheet=null;
	}
	
	/**
	 * @return the repaintOnMouseMoved. For further information visit {@link #repaintOnMouseMoved}
	 */
	public boolean isRepaintOnMouseMoved() {
		return repaintOnMouseMoved;
	}

	/**
	 * @param repaintOnMouseMoved the repaintOnMouseMoved to set
	 */
	public void setRepaintOnMouseMoved(boolean repaintOnMouseMoved) {
		this.repaintOnMouseMoved = repaintOnMouseMoved;
	}

	/**
	 * @return the map. For further information visit {@link #map}
	 */
	public GameMap getMap() {
		return map;
	}

	/**
	 * @param map the map to set
	 */
	public void setMap(GameMap map) {
		this.map = map;
		this.initMapSheet();
		this.repaint();
	}
	
	/**disegna la lista di segmenti specificata da {@link #points} unita dal punto speicificato da {@link #mouse}
	 * 
	 * @param g il contesto grafico da usare
	 */
	private void drawPointList(Graphics2D g){
		if (points.size()==0){
			return;
		}
		for (int i=1;i<this.points.size();i++){
			g.drawLine(
					this.points.get(i-1).x*this.map.getMapresolution(),this.points.get(i-1).y*this.map.getMapresolution(),
					this.points.get(i).x*this.map.getMapresolution(),this.points.get(i).y*this.map.getMapresolution());
		}
		g.drawLine(
				this.points.lastElement().x*this.map.getMapresolution(),this.points.lastElement().y*this.map.getMapresolution(),
				this.mouse.x,this.mouse.y);
	}

	/**
	 * 
	 * @return il numero di punti da inserire 
	 */
	public int getPoints(){
		return this.points.size();
	}
	/**aggiunge un punto nella serie di segmenti consecutivi del Display
	 * 
	 * @param p
	 */
	public void addPoint(Point p){
		this.points.add(p);
		this.repaint();
	}
	/**rimuove l'ultimo punto nella serie di segmenti consecutivi del Display
	 * 
	 */
	public void removelastPoint(){
		this.points.remove(this.points.size()-1);
		this.repaint();
	}
	/**elimina tutti i punti
	 * 
	 */
	public void clearPoints(){
		this.points.clear();
		this.repaint();
	}
	
	/**disegna una MapObject secondo le specifiche contenute al suo interno. Il metodo di disegno è disegnare il
	 * renderer <strong>direttamente</strong>sull Graphics passata, senza passare per alcuna immagine intermedia
	 * 
	 * @param g il decive context usato
	 * @param mapobject il mapobject da disegnare
	 * @param grayfilter TRUE se si vuole convertire il MapObject in una scala di grigi
	 */
	private void drawMapObject(Graphics2D g2, MapObject mapobject,boolean grayfilter){
		BufferedImage img=grayfilter?this.getGrayImage(mapobject.getRenderer().drawRenderer(g2)):mapobject.getRenderer().drawRenderer(g2);
		g2.setPaint(null);
		g2.setClip(null);
		g2.drawImage(
				img, 
				mapobject.getX(), 
				mapobject.getY(),
				mapobject.getShape().getBounds().width,
				mapobject.getShape().getBounds().height,
				this);
	}
	
	/**disegna il MapLayer passato come parametro. I MapLayer vengono disegnati attraverso
	 * l'uso di trasformazioni geometriche (AffineTransform e .scale)
	 * 
	 * @param g il device context su cui disegnare il MapLayer
	 * @param maplayer il maplayer da usare
	 * @param grayfilter TRUE se si vuole usare una scala di grigi, FALSE altrimenti
	 */
	private void drawMapLayer(Graphics2D g2, MapLayer maplayer,boolean grayfilter){
		//per trasformazione intendo trasformazione geometrica, quella che la Gobetti ci ha insegnato!!
		BufferedImage img=grayfilter?this.getGrayImage(maplayer.getRenderer().drawRenderer(g2)):maplayer.getRenderer().drawRenderer(g2);
		TexturePaint texture=new TexturePaint(img,new Rectangle(0,0,1,1));//img.getWidth(),img.getHeight()));
		
		g2.setPaint(texture);
		//g2.setClip(maplayer.getShape());
		g2.fill(maplayer.getShape());
	}

	/**disegna alcuni strati secondo alcuni criteri; dato lo <tt>stratum</tt>, cioè lo strato in modifica dall'utente, si ha
	 * <ul>
	 *  <li>tutti gli strati inferiori allo <tt>stratum</tt> vengono disegnati in scala di grigi</li>
	 *  <li>lo strato in modifica viene disegnato normalmente</tt>
	 *  <li>tutti gli strati superiori allo <tt>stratum</tt> non vengono disegnati</li>
	 * </ul>
	 * @param g il device-context da utilizzare
	 */
	private void drawVisibleForegrounds(Graphics2D g){
		if(this.totalStrata.getValue() == 0){//se non ci sono strati non bisogna disegnare alcun foreground
			return;
		}
		for(int i = 0; i <= this.stratum.getValue(); i++){
			for(Foreground f : map.getStratum(i).values()){//devo, per ogni strato, disegnare prima tutti i MapLayers e solo poi tutti i MapObjects!
				if(f instanceof MapLayer){
					this.drawMapLayer(g, (MapLayer)f,i<this.stratum.getValue());
				}
			}
			for (Foreground f: map.getStratum(i).values()){
				if (f instanceof MapObject){
					this.drawMapObject(g, (MapObject)f,i<this.stratum.getValue());
				}
			}
		}
	}
	/**data un'immagine a colori ritorna la stessa immagine in tonalità di grigio
	 * 
	 * @param colorimage l'immagine colorata
	 * @return l'immagine in bianco e nero
	 */
	private BufferedImage getGrayImage(BufferedImage colorimage){
		
		//TODO trovar eun metodo migliore... per il momento ci possiamo accontentare ;)
		//**************** tramite GrayFilter ******************************
		/*ImageFilter filter = new GrayFilter(false,50);
		ImageProducer producer = new FilteredImageSource(colorimage.getSource(), filter);  
		return this.createImage(producer);*/
		//***************** tramite BuffererImage (ma non va l'alpha channel) ****************************
		BufferedImage grayImage=new BufferedImage(colorimage.getWidth(this),colorimage.getHeight(this),BufferedImage.TYPE_BYTE_GRAY);
		grayImage.getGraphics().drawImage(colorimage,0,0,this);
		return grayImage;
	}
	
	/*
	 * (non-Javadoc)
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 * 
	 * spiegazione:
	 * 
	 * paintComponent viene eseguito fin dall'inizio, quando ancora this.map è NULL e sopratuttto quando ancora g non è ancora stato
	 * creato (vedi disquisizioni su update()). Per questo all'inizio si verifica che g sia NULL o meno. Se g è NULL allora non si può proprio
	 * disegnare, quindi si esce direttamente. Dato che il display si basa sulla tecnica di offScreenImage (disegno prima su un foglio
	 * di carta esterno e solo quando ho il disegno completo disegno sul Display), anche quando tale foglio di carta esterno è NULL
	 * il Display non può essere disegnato. Se queste 2 componenti non sono NULL allora il Display può essere (almeno per noi) disegnato.
	 * Dire che il Display può essere disegnato non significa dire che il Display debba disegnare qualcosa: in fatti la this.map potrebbe
	 * benissimo essere nulla. Se è questo il caso allora non ci resta che disegnare uno sfondo a caso (io ho preso lo STESSO del background neutro,
	 * ma è facilmente modificabile!). Se invece la mappa esiste, non ci resta che disegnarla!
	 * Disegnare la mappa significa disegnare essenzialmente 3 cose:
	 *  1) disegnare il renderer del background
	 *  2) disegnare i renderer dei foreground (MapObject e MapLayer)
	 *  3) disegnare il poligono che si sta creando
	 * fatti questi passaggi basta incollare la offScreenImage sul Display e il gioco è fatto.
	 * --> Per disegnare il Background basta ripetere il Renderer del background tutte le volte necessarie sullo schermo
	 * --> Per disegnare i MapObject e i MapLayer invece la questione è più complessa: non basta disegnare secondo le coordinate specificate
	 * da Foreground.shape in quanto tali cooridnate assumono un significato diverso a seconda del fatto che sia MapObject o MapLayer:
	 * la shape di MapObject indicano coordinate relative a (x;y) mentre quelle di MapLayer sono ASSOLUTE. Inoltre tali cooridnate vanno moltiplicate
	 * per la mapResolution della mappa (cioé bisogna applicare un'omotetia). Un altro problema riguarda i MapLayer: come facciamo a disegnare forme non
	 * poligonali? Per risolvere questi problemi utilizziamo Graphics2D. Quando Graphics2D disegna tiene conto di 4 fattori:
	 * --> Clip: la clip è l'area che vogliamo tenere aggiornata. Tutto ciò fuori dalla Clip non verrà modificato
	 * --> Paint: indica come dobbiamo riempire una forma generica (Shape); tale riempimento può essere un Color, un TexturePaint o un GradientPaint
	 * --> Fill: riempie una data Shape
	 * --> Transform: indica l'affinità (trasformazione geometrica generica) che dobbiamo applicare ad ogni coordinata che ci viene passata. Molto utile per risolvere
	 * l'omotetia
	 */
	public void paintComponent(Graphics g){
		if ((g==null)||(this.mapSheet==null)){
			//il componente non può neanche essere disegnato
			return;
		}
		//il componente può essere disegnato
		Graphics2D gSheet=(Graphics2D)this.mapSheet.getGraphics();
		if (this.map==null){//la mappa non è ancora stata cablata --> sfondo neutro
			this.drawBackground(gSheet, null);
			return;
		}
		//la mappa è stata cablata --> bisogna disegnarla
		//******************** INIZIALIZZAZIONE *********************
			//salvataggio oggetti che vado a modificare
				Shape oldshape=gSheet.getClip();
				AffineTransform oldtransform=gSheet.getTransform();
				Paint oldpaint=gSheet.getPaint();
		//******************** BACKGROUND DRAWING *************************
				this.drawBackground(gSheet,this.map.getBackground());
		//******************** FOREGROUND DRAWING ********************************
			//creo una trasformazione geometrica per effettuare l'omotetia per ogni cooridnata che userò
				AffineTransform scaling=gSheet.getTransform();
				scaling.scale(this.map.getMapresolution(),this.map.getMapresolution());
				gSheet.transform(scaling);
				this.drawVisibleForegrounds(gSheet);
			//recupero gli oggetti che ho modificato nel disegno
				gSheet.setClip(oldshape);
				gSheet.setTransform(oldtransform);
				gSheet.setPaint(oldpaint);
		//******************** DISEGNO DELL'IPOTETICO POLIGONO ***********************************
			//FIXME è lento... trovar eun algoritmo più performante!
			this.drawPointList(gSheet);
		//******************** DISEGNO DELLO SHEET SUL DISPLAY ******************************
			g.drawImage(mapSheet, 0, 0, this);
		//******************** FINALIZATION **********************************
	}
	
	/** Controlla se la mappa è null o meno.
	 * @return true se la mappa è null, false altrimenti. */
	public boolean isMapNull(){
		return (this.map == null ? true : false);
	}
	
	/**imposta la mappa locale a quella che ha spedito l'evento intercettato, quindi effettua un repaint del Display stesso
	 * 
	 * @param arg0 l'evento intercettato
	 */
	private void updateRepaintLocalMap(MapEvent arg0){
		this.map=(GameMap)arg0.getSource();
		this.repaint();
	}
	
	@Override
	public void backgroundChanged(MapEvent<Background> arg0) {
		this.updateRepaintLocalMap(arg0);
	}

	@Override
	public void foregroundChanged(MapEvent<Foreground> arg0) {
		this.updateRepaintLocalMap(arg0);
	}
	
	@Override
	public void mapresolutionChanged(MapResolutionEvent arg0) {
		this.map=(GameMap)arg0.getSource();
		this.initMapSheet();
		if (this.map!=null){
			this.setPreferredSize(new Dimension(this.map.getWidth()*this.map.getMapresolution(),this.map.getHeight()*this.map.getMapresolution()));
			this.repaint();
		}
	}

	@Override
	public void widthChanged(MapWidthEvent arg0) {
		this.map=(GameMap)arg0.getSource();
		this.initMapSheet();
		if (this.map!=null){
			this.setPreferredSize(new Dimension(this.map.getWidth()*this.map.getMapresolution(),this.map.getHeight()*this.map.getMapresolution()));
			this.repaint();
		}
	}

	@Override
	public void heightChanged(MapHeightEvent arg0) {
		this.map=(GameMap)arg0.getSource();
		this.initMapSheet();
		if (this.map!=null){
			this.setPreferredSize(new Dimension(this.map.getWidth()*this.map.getMapresolution(),this.map.getHeight()*this.map.getMapresolution()));
			this.repaint();
		}
	}
	
	/**inizializza la MapSheet in modo che possa ricevere una nuova mappa da disegnare.
	 * Questo metodo fa le seguenti azioni:
	 * <ul>
	 *  <li>setta la width e la height della {@link #mapSheet} in modo che sia uguali a quelle della {@link #map}</li>
	 *  <li>pulisce il {@link #mapSheet} in modo che sia (per esempio) nero</li>
	 * </ul>
	 * 
	 * il metodo si preoccupa di creare un nuovo {@link #mapSheet}. Può accadere però che questa operazione fallisca a causa della
	 * gigantesca quantità di memoria richiesta. In questo caso il {@link #mapSheet} viene portato a NULL
	 */
	private void initMapSheet(){
		System.out.println("tento di creare la mappa...");
		try{
			this.mapSheet=new BufferedImage(this.map.getWidth()*this.map.getMapresolution(),this.map.getHeight()*this.map.getMapresolution(),BufferedImage.TYPE_4BYTE_ABGR);
			System.out.println("successo!");
		}catch (OutOfMemoryError error){
			//width o height troppo grosse! (capita con 1500x1500x1)
			System.out.println("out of memory!");
			this.mapSheet=null;
			this.map=null;
		} catch (NegativeArraySizeException error){
			System.out.println("out of memory!");
			this.mapSheet=null;
			this.map=null;
			//troppo grossa (capita con 20x20x50)
		}
		//TODO mettere una variabile (o costante) dove incamerare lo sfondo neutro del Display
	}
	
	private void drawBackground(Graphics2D g,Background background){
		BufferedImage img=(background==null?this.defaultBackground:background.getRenderer().drawRenderer(g));
		TexturePaint texture=new TexturePaint(img,new Rectangle(0,0,img.getWidth(),img.getHeight()));
		g.setPaint(texture);
		g.fill(this.getBounds());
	}
	
	/**dipinge l'intero Display di nero.
	 * 
	 * @param g
	 */
	private void fillNeutral(Graphics g){
		this.setBackground(Color.BLACK);
	}
	
	// Metodi da specificare per aver implementato Scrollable.
	
	@Override
	public Dimension getPreferredScrollableViewportSize() {
		return this.getPreferredSize();
	}

	@Override
	public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
		return this.map==null?this.DEFAULTSCROLLINCREMENT:this.map.getMapresolution();
	}

	@Override
	public boolean getScrollableTracksViewportHeight() {
		if (getParent() instanceof JViewport) {
			return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
		}
		return false;
	}

	@Override
	public boolean getScrollableTracksViewportWidth() {
		if (getParent() instanceof JViewport) {
			return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
		}
		return false;
	}

	@Override
	public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
		return (orientation==SwingConstants.HORIZONTAL? visibleRect.width:visibleRect.height);
	}
	
	// Metodi da specificare per aver implementato MouseMotionListener.
	
	@Override
	public void mouseDragged(MouseEvent e) {
		Rectangle rectangle = new Rectangle(e.getX(), e.getY(), 1, 1);
		this.scrollRectToVisible(rectangle);
	}

	@Override
	public void mouseMoved(MouseEvent arg0) {
		if (this.repaintOnMouseMoved){
			this.mouse=arg0.getPoint();
			this.repaint();
		}
	}
	
}
